package com.mini.mybatis.mybatisministep09.binding;

import com.mini.mybatis.mybatisministep09.mapping.MappedStatement;
import com.mini.mybatis.mybatisministep09.mapping.SqlCommandType;
import com.mini.mybatis.mybatisministep09.sqlsession.SqlSession;
import com.mini.mybatis.mybatisministep09.sqlsession.configuration.Configuration;

import java.lang.reflect.Method;
import java.util.*;

/**
 *  有没有发现一个问题，很多框架都喜欢在构造方法中去初始化一些方法，构造一些对象
 *  可能是很多层级的，你new了一个最外层的对象，它内部通过级联调用构造方法，把每一个
 *  涉及到的类都初始化好了，后续这些对象都等待你后续调用，这也是为什么好多内层东西你都不知道
 *  它在哪一步初始化的？理解这种思想，能否运用到自己平时的工作中呢？Harvey~
 */
public class MapperMethod {

    private SqlCommand sqlCommand;

    private MethodSignature methodSignature;

    public MapperMethod(Configuration configuration, Class<?> mapperInterface, Method method){
        this.sqlCommand = new SqlCommand(configuration,mapperInterface,method);
        this.methodSignature = new MethodSignature(configuration,method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result = null;
        switch (sqlCommand.getType()){
            case UNKNOWN:
                break;
            case INSERT:
                break;
            case DELETE:
                break;
            case UPDATE:
                break;
            case SELECT:
                // 这一步不直接把参数数组向下传递，而是处理完后交给sqlSession
                // 这里是mapper接口中的参数列表，不要跟sql中的parameterType搞混，sql那边的解析，在构建sqlSessionFactory的时候就已经解析过了
                Object params = methodSignature.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(sqlCommand.getId(),params);
                break;
            default:
                throw new RuntimeException("unable process sqlCommand: "+sqlCommand.id);
        }
        return result;
    }


    public static class SqlCommand{

        private String id;
        private SqlCommandType type;

        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method){
            String statement = mapperInterface.getName() + "." + method.getName();
            MappedStatement mapStatement = configuration.getMappedStatement(statement);
            this.id = mapStatement.getId();
            this.type = mapStatement.getSqlCommandType();
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public SqlCommandType getType() {
            return type;
        }

        public void setType(SqlCommandType type) {
            this.type = type;
        }
    }



    /**
     * 方法签名
     */
    public static class MethodSignature{

        private final SortedMap<Integer, String> params;

        public MethodSignature(Configuration configuration, Method method){
            this.params = Collections.unmodifiableSortedMap(getParams(method));
        }


        public Object convertArgsToSqlCommandParam(Object[] args) {
            final int paramCount = params.size();
            if (args == null || paramCount == 0) {
                //如果没参数
                return null;
            } else if (paramCount == 1) {
                return args[params.keySet().iterator().next()];
            } else {
                // 否则，返回一个ParamMap，修改参数名，参数名就是其位置
                final Map<String, Object> param = new ParamMap<Object>();
                int i = 0;
                for (Map.Entry<Integer, String> entry : params.entrySet()) {
                    // 1.先加一个#{0},#{1},#{2}...参数
                    param.put(entry.getValue(), args[entry.getKey()]);
                    // issue #71, add param names as param1, param2...but ensure backward compatibility
                    final String genericParamName = "param" + (i + 1);
                    if (!param.containsKey(genericParamName)) {
                        /*
                         * 2.再加一个#{param1},#{param2}...参数
                         * 你可以传递多个参数给一个映射器方法。如果你这样做了,
                         * 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等。
                         * 如果你想改变参数的名称(只在多参数情况下) ,那么你可以在参数上使用@Param(“paramName”)注解。
                         */
                        param.put(genericParamName, args[entry.getKey()]);
                    }
                    i++;
                }
                return param;
            }
        }


        /**
         *  获取一个方法的参数treemap
         *  注意，获取的不是实际的参数的名和值，而是方法数组下标
         *  如：参数列表为 Integer teamId, String teamName
         *     treemap为 [{"0","0"},{"1","1"}]
         *     外层获取真实参数根据args[0] 获取到teamId args[1] 获取到teamName
         *     如果用到了@Param注解，则可通过方法名获取，也是在这一步解析的
         */
        private SortedMap<Integer, String> getParams(Method method) {
            // 用一个TreeMap，这样就保证还是按参数的先后顺序
            final SortedMap<Integer, String> params = new TreeMap<Integer, String>();
            final Class<?>[] argTypes = method.getParameterTypes();
            for (int i = 0; i < argTypes.length; i++) {
                String paramName = String.valueOf(params.size());
                // 不做 Param 的实现，这部分不处理。如果扩展学习，需要添加 Param 注解并做扩展实现。
                params.put(i, paramName);
            }
            return params;
        }



    }

    /**
     * 静态内部类，更严格的get方法，如果没有相应的key，直接报错，自定义自己的map
     * @param <V>
     */
    public static class ParamMap<V> extends HashMap<String,V>{

        @Override
        public V get(Object key) {
            if (!super.containsKey(key)){
                throw new RuntimeException("Parameter '" + key + "' not found. Available parameters are " + keySet());
            }
            return super.get(key);
        }
    }

}
